in this video we're going to handle
simple spreadsheet animation in a period
TCS game by using draw mesh instance
we're going to prepare on our data in
super fast jobs and do very few draw
calls in order to achieve an insane
result of a hundred thousand animated
sprites let's begin
[Music]
hello and welcome I'm your code monkey
and this channel is all about helping
you learn how to make your own games
with in-depth tutorials made by a
professional indie game developer so if
you find the video helpful consider
subscribing so here's what we want to
create a nice simple spreadsheet
animation system however we're going to
be using drama SH instance so instead of
having just a handful of sprites here it
is we have an insane amount of sprites
being rendered all of them individually
animated and if we look at the stats you
can see we have a hundred thousand and
everything is running at 30 frames per
second so this is our goal let's get to
it so here we are in our scene this
video is a continuation of the previous
video on how to do simple spreadsheet
and mission so go check out that video
first if you haven't already there's a
link in the description in that video we
made some entities and set them up to
use simple spreadsheet animation so this
is the result we have entities being
animated using a spreadsheet the
animation is handled by this component
in here as you can see it stores the
current frame the total frame the
various timers and also stores the UV
and the matrix then we have this system
in here which is animating all of that
data it increases the time and if needed
it increases the current frame then it
calculates the UV and the matrix so this
whole thing is running inside a job
which means that this code runs super
fast
even on thousands of sprites and finally
over here we have our render system here
we're doing a for each on every entity
and we are using graphics animation or
to draw a mesh per every entity we're
using the material property block in
order to pass values into our shader
specifically a vector for containing our
UV coordinates so every single unit is
using the same mesh same material the
only difference is handled by the shader
alright so this is what we have so far
and here as you can see it does indeed
work we have our units nicely animated
however the performance isn't as good as
it can be don't limit in this system is
around 12,000 units which makes it run
at 30 frames per second that's already a
pretty massive number especially when
compared to normal game object but we
can sound you better the biggest
bottleneck is here in our rendering
system we're cycling through every
single entity and doing a draw mesh so
with 10000 entities we have the cycle
running 10,000
times on the main thread and drawing
10,000 meshes being single thread means
this code will get slower and slower the
more units we add now as I mentioned in
the previous video there is another way
in order to draw meshes instead of using
graphics are draw mesh we can use
graphic start draw mesh instance this
function as you can see receives an
array of matrixes so we can prepare the
array containing the matrices for on VAR
units and the material property block
for own the UVs and we do a single drop
so let's try this out let's icon through
all of our entities however instead of
using entities dot for each and let's
make a entity query then let's grab our
array of our animation data now we can
cycle through our array and here let's
create our list of matrixes for all of
our units so out here make a list of
matrix 4x4 in here we just add them all
right so now we have all the information
needed so we do a graphic start Romesh
instance going to use the same mesh
all right so here is our code using draw
mesh instance so we can now get rid of
this one down here and here we have our
modified code so we grab an entity query
of all the entities with our animation
data we grab an array containing all of
that component data we create and
populate a matrix and a UV list then we
create the things that we're going to
need and finally we do a single draw
call using our entire list and you can
list okay now let's go here in order to
spawn just 10 units and let's see any up
here we are we have 10 units being
displayed and here in the stats window
you can see just a single batch and nine
saved by batching alright so everything
seems great now let's try drawing 10,000
units and as soon as we do we come
across an error now one of the
limitations of drama instance is that it
can only draw a thousand and twenty
three meshes per single comp so
essentially we need to split our calls
into various batches so let's do that
over here in our render let's define our
slice count and let's put 10 23 for our
limit and let's cycle through our
entities in here and let's complete the
slice size so we're counting the slice
size since the last slice will be
smaller than 2023 okay and now in here
we make our own lists
then we cycle through our slice eyes and
we grab on the index starting on the I
plus J so on this slice and finally we
set our UV array and now we can draw our
mesh alright so just like that we are
now going to have one draw machine since
call per each slice of 10:23 entities
just one thing in here we are not
increasing the eye by just one but by
our slice count so essentially if we
have just two thousand entities this
will run only twice okay let's see
and yup here we are there's no error and
our meshes are now being displayed all
right awesome however you can already
see another issue specific to draw mesh
instance by using this function we are
essentially bypassing all the entity
sorting and cone meaning we must handle
that ourselves so when you create our
matrix and UV rays we need to sort them
correctly we need to make sure that the
units on a higher position stay on the
beginning of the array and the ones down
here on the end of the array so that
everything is correctly sorted so let's
see how we can do that in the simplest
and most straightforward way possible so
over here we're doing our cycle setting
up our matrix and UV rays in order to
this point and we are doing it based on
the array containing our animation data
so before we do that we need to store
this array by our entity position so
let's go all the way up here and our
aquarium let's also grab the translation
and in here let's do some very simple
sorting
all right so this should do it we cycle
through our list and we are sorting them
based on the position that one so when
two T's above will be behind entities
below
all right so let's test however before
we do that let's go up here and test
with only a hundred units you'll see why
in a bit and let's see so here we are
and yep everything is looking great
we have all our units animated
individually and all of them are being
displayed correctly all the meshes are
correctly sorted and transparency isn't
working great so this is awesome this is
exactly what we want however if we look
at the stats window over here you can
see why we're doing this test with just
a hundred units even with so few units
we are taking five milliseconds in order
to run this scene that is quite slow and
wouldn't work with even a couple
thousand units so far we've been doing
everything very linearly here in our
render code you can see that we're doing
all of this just on the main thread so
we're not benefiting from the job system
we're doing all the sorting right here
on the main thread this specific type of
sorting will increase in complexity
exponentially as we add more units and
over here when we are creating the
matrix and the UV we're also just doing
a very simple cycle going through every
single unit so now that we have the core
functionality working it's time to make
it work by taking advantage of the job
system and improve our performance over
here the biggest bottleneck by far is
the sorting here we cycle through every
single entity and for each entity we are
cycling through every single entity so
with just a hundred entities we are
essentially running this code a hundred
to the power of a hundred times which is
already an insane number the more
entities we get the more insane this
gets so our biggest challenge here is to
figure out a way to do sorting without
having to test every entity against
every other entity now in here I won't
go over the approach that I found the
best after doing a lot of testing I
don't know if my approach is the
absolute best one but the end result is
two hundred thousand animated entities
so that's quite good the way we're going
to do it is essentially split the screen
into very
rectangle slices from top to bottom and
we sort each slice individual that way
each slice has fewer entities so it's
faster and multiple slices can be sorted
at the same time all right so let's get
to it let's duplicate the system just so
we have something to compare with later
so in here just duplicate this and make
this one the old system here just rename
it to the old one and in here we can add
the attribute disabled out of creation
so this system will not run okay good so
we can now modify this one now in here
the first thing we're going to need is a
struct to hold whatever that we're going
to need in order to render so in here
let's make a private struct call it
render data let's store a reference to
our entity we're also going to need a
full 3/4 D position and we're also going
to need the matrix and obviously a UV
okay so this is the struct that we're
going to work with inside all of our
jobs we need the position for sorting
and the matrix and UV for rendering so
let's start off with a job to split our
entities into just two slices so we're
going to have a job cycling through all
our entities grabbing their position and
animation data now in here the way we're
going to split our entities is by using
native cues so we're going to have two
fields for each cue we're using
concurrent so we can concurrently write
in various threads now the goal in here
is essentially if a entity is on the top
half of the screen it will be placed on
the native cue one differences on the
bottom it will be placed on the second
one so we also need to know where
exactly is the top and the bottom
so if you got a float for the top part
of our screen a second slice running
from top so in this case is going to be
right in the middle and the bottom of
our screen the reason we're not simply
defining the top middle and bottom is
because later we're going to slice it
into more than just two parts now in
here inside our execute method first we
do some simple calling to see if it's
even within these valves so we have our
family if it's above the bottom and
under the top now if it is valid we just
do a very simple if all right so here we
have very simple we create our render
data based on the information in here
then we simply check if it's under the
second slice then we place it on the
second q if not we place it on the first
view all right so that's it this is our
very simple job it simply goes through
every entity and place it on one of two
queues so at the end we have our entity
sorted into either the top or the bottom
half of the screen now let's run this
channel let's go down here into our
update and first we're going to make our
adherence
so we have our two cues now we need to
calculate the slice values based on our
camera so grab the main camera the
camera position we complete the bottom
based on the camera orthographic size
same thing for the top and then our
second slice which is right down the
middle
alright now let's schedule and complete
our job all right so we are creating our
job passing in all the valleys that it
needs schedule and complete okay so
let's do a very quick test let's do a
simple debug log to see how many
entities are on the first slice and how
many on the second slice so a debug log
negative Q one dot count and same thing
for t2 all right let's see and you up
here in the console we can already see
46:54 so we have 46 entities on the top
of the screen and 54 on the bottom half
ok great so we have correctly split our
entities into two separate slices so
essentially right now we can sort both
slices at the same time and since each
of them has fewer units they run even
faster so you can already see you are we
going with this approach so after doing
all of our slicing we now have two
queues one for each slice however we
can't directly sort a native queue so we
need to take these queues and convert
them into native arrays that we can then
sort so let's make a job to do just that
all right so here this very simple job
we take a heat we take an array we try
to the queue whilst the queue has
elements and we similar place them on
the array so very simple now let's run
our job first we need to create our race
for the size of the right won't be the
same size as the queue so we go into the
queue and it we're at the account
alright here it is so we first define
our race with the same size as our
queues we create our jobs we schedule
all the jobs and put the job handle in a
job Hemel a right and once both of them
have been scheduled we complete both
jobs at the same time so right now down
here we already have two arrays instead
of two queues so with those we can now
sort them so let's make a job to do just
that let's sort the same way that we did
previously
and now let's run both jobs so over here
we now have our both arrays correctly
sorted now all we need to do is draw
them so down here let's get rid of our
previous sorting and in our drug code
let's modify it to be more efficient and
work with simple arrays let's make it
work with a matrix in UV array so no
references in the animation that so in
here in order to create a new matrix
array we need to know essentially how
many units are visible since we're doing
calling not necessarily every single
entity won't be visible so let's
calculate that do one end for the
visible entity total and here it will be
essentially the total of our native
array one was native right too so we
create an array of that size now let's
draw our entities using these arrays so
let's assume that these arrays are
completely filled with all the entities
so inside we no longer cycle through our
animation data instead we make some
basic arrays out here
so we have these two normal rays and now
inside our cycle we can simply use a
copy to so use the function on the
native array and we copy from our source
native array so that's the matrix array
and here we can pick the source index so
we are using this super fast function in
order to capture slices from this array
and then we just draw our mesh alright
so this is it
let's also remember to dispose of all
our native arrays so now we have our
draw code in here working perfectly
based on these two arrays so in terms of
the drawing we don't care what happens
previously we just care that we have
these two film the rates now all that's
left is in order to take our sliced
arrays and merge them into these two
arrays so let's make a job to do that
merger let's go up here and now that our
private struct this won't be the film
arrays
all right so here in our job we have our
origin native array and the matrix and
UV ray which are the phone arrays
then we also receive a starting index so
the first ray will be on a certain index
position then the second array will be
on another starting index position so
since we have multiple jobs writing to
both of these arrays we need to disable
the container safety restrictions and
this attribute is also inside unity
collections all of'em and safe now when
using this you need to make very sure
that you are very careful with your
indexes you need to be careful to not
cause any race conditions so now let's
run our job so down here we have our two
arrays sorting then we create our matrix
n UV array and it's in here that we need
to fill them up
so we have our two jobs in order to take
our native array one and two and place
them in the matrix array in UV array
again very careful with the starting
index as you can see the first one won't
start on the starting index of zero but
the second one won't start at the end of
the index for the first array so after
this after these jobs have been
completed our data is now sorted and
placed in a single phone array one for
the matrix and one for the UV now in
here we wrote a lot of code but
hopefully this was easy to follow along
the strategy behind it is quite simple
so the first thing we do is we have a
job in order to count and sort our
entities into various slices so in this
case we just have two slices so one for
the top part of the screen and one for
the bottom part of the screen so this
job places our entities in the correct
queue then before we can do sorting we
need to take the entities from that
queue and place them in an array so we
have this job to do exactly that so down
here we now have our two arrays ready so
we have this job in order to sort our
arrays by position so we schedule in
both jobs and then we run them both so
essentially the top and the bottom won't
be sorted at the same time on different
cores and then once we have them on
Sargent we create full arrays for our
matrixes and our UVs
and then we have this job which takes
our individual arrays and merges them
all into one single array and finally
when we have these two arrays on film
with the correct data down here we have
our draw code so we go through our race
in slices of 10 23 since that's the
limitation of drama instance we do a
simple copy from our full array into our
slice array and we render the whole
thing and obviously one thing we can do
in order to get extra performance is to
simply add our birth compound into all
these jobs since all of them are births
comparable so add that one down that one
and that one alright so let's see
alright so here we are with our new
system and all of our entities are being
drawn so the code is working great now
it's time to see exactly how much of a
speed difference we made between the old
system with no multi-threading and this
one with multi-threading so here we have
the own system running with 500 units
and as you can see a hundred
milliseconds per frame now I see the new
system and here we are with the new
system rendering those same 500 units in
just 1.5 to 2 milliseconds thanks to all
the benefits from multi-threaded code ok
so this is already pretty awesome now
over this past week I've spent a lot of
time working on this code and making it
run as fast as possible on every single
step of the way so let's see the
absolute best performance I've managed
to get so here is the final system
running and over here you can view the
stats as you can see we have a hundred
thousand individually animated sprites
running at 30 frames per second so this
is pretty much the definition of
insanity so let's see how this final
system works and you'll be able to see
the same structure that we built the
strategy in here is very much the same
we have the camera viewport being cut
into multiple slices however instead of
having just two slices we have 20 so in
here you can see our Cowan sword job we
have our 20 slices and 20 Q's and all
these ifs in order to place everything
in the correct queue and we are also
calling for the X minimum and maximum so
over here in the scene view you can see
the culling in action as you can see all
of them are being drawn but if I move
the camera as you can see the entities
that go away on the culling on the
minimum X they get hidden and same thing
on down so it only actually shows the
entities and that need to be shown so we
have our job using our 20 slices and
here we have our update we are going
through our position sizes which are 20
getting the camera and calculating all
of these slices then we create all the
arrays based on those slices we use the
same native cue to a ray job in order to
convert all of those cues into all of
those arrays then same thing in order to
sort then we have our individual arrays
we put together all those 20 arrays into
two separate arrays and then we have our
simple draw code so as you can see the
structure is very much the same that we
did only with a bunch more improvement
so that it runs insanely fast again here
we have a hundred thousand individually
animated sprites running at 30 frames
per second and we can also look at the
profiler so here we have our probe
as you can see everything is running
insanely fast and that's it here's our
insanely fast simple spreadsheet
animation system as always you can
download the project files any tony's
from unity code monkey comm if you liked
the video subscribe the channel for more
unity tutorials
post any questions i have in the
comments and I'll do my best answer
alright see you next time
[Music]